package com.google.gwt.gwtpages.client;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import com.google.gwt.event.logical.shared.ValueChangeEvent;
import com.google.gwt.event.logical.shared.ValueChangeHandler;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.gwtpages.client.event.PageShownEvent;
import com.google.gwt.gwtpages.client.message.DefaultPageMessageHandler;
import com.google.gwt.gwtpages.client.page.ApplicationPresenter;
import com.google.gwt.gwtpages.client.page.AsyncPageCallback;
import com.google.gwt.gwtpages.client.page.DefaultPageLoadingHandler;
import com.google.gwt.gwtpages.client.page.LoadedPageContainer;
import com.google.gwt.gwtpages.client.page.PageEventHandler;
import com.google.gwt.gwtpages.client.page.loader.PageLoadCallback;
import com.google.gwt.gwtpages.client.page.loader.PageLoader;
import com.google.gwt.gwtpages.client.page.parameters.PageParameters;
import com.google.gwt.gwtpages.client.page.parameters.SimplePageParameters;
import com.google.gwt.gwtpages.client.page.parameters.SimpleTokenizer;
import com.google.gwt.gwtpages.client.page.parameters.Tokenizer;
import com.google.gwt.user.client.History;

/**
 * Controller class used to move from 1 page to another within a GWT Pages
 * application. When using this class, the
 * {@link GWTPagesSettings#init(PagePresenter, PageLoader, com.google.gwt.event.shared.HandlerManager)}
 * method must be called first. There are several different gotoPage methods
 * which can be used depending on the context.
 * 
 * When the application initially loads, you also usually want to call
 * {@link Pages#showStartPage(boolean)}
 * 
 * @author Joe Hudson
 */
public class Pages implements ValueChangeHandler<String> {

	private static Pages instance;

	private Settings settings = new Settings();
	protected HandlerManager eventBus;

	private boolean keepGoing = true;
	private GotoPageCommand lastCommand;

	// local cache
	private Map<String, LoadedPageContainer> pageMap = new HashMap<String, LoadedPageContainer>();

	public static Pages get() {
		if (null == instance)
			instance = new Pages();
		return instance;
	}

	public static Pages init(ApplicationPresenter applicationPresenter,
			PageLoader pageLoader, HandlerManager bus,
			boolean monitorHistoryEvents) {
		Pages pages = new Pages(applicationPresenter, pageLoader, bus,
				monitorHistoryEvents);
		instance = pages;
		return pages;
	}

	public Pages(ApplicationPresenter applicationPresenter,
			PageLoader pageLoader, HandlerManager bus,
			boolean monitorHistoryEvents) {
		settings.pagePresenter = applicationPresenter;
		settings.pageLoader = pageLoader;
		eventBus = bus;
		init();
		if (monitorHistoryEvents)
			History.addValueChangeHandler(this);
	}

	protected void init() {
		settings.pageLoader.init(this);
		settings.pagePresenter.init(this);
	}

	public Pages() {
		instance = this;
	}

	/**
	 * Go to the start page.
	 * 
	 * @param useHistoryToken
	 *            true if the history token should be evaluated to obtain the
	 *            page token and false to use the standard default page
	 */
	public void showStartPage(boolean useHistoryToken) {
		String token = PageLoader.PAGE_DEFAULT;
		if (useHistoryToken)
			token = History.getToken();
		if (null == token)
			token = PageLoader.PAGE_DEFAULT;
		onNewPage(token, true);
	}

	/**
	 * Return the {@link GotoPageCommand} used to navigate to the page
	 * represented by the page token
	 * 
	 * @param pageToken
	 *            the page token
	 */
	public GotoPageCommand gotoPage(String pageToken) {
		return new GotoPageCommand(pageToken, this);
	}

	/**
	 * Return the {@link GotoPageCommand} used to navigate to the page
	 * represented by the page token
	 * 
	 * @param pageToken
	 *            the page token
	 * @param convienance
	 *            method for using
	 *            {@link GotoPageCommand#addParameter(Serializable)}
	 */
	public GotoPageCommand gotoPage(String pageToken, Serializable... params) {
		GotoPageCommand cmd = new GotoPageCommand(pageToken, this);
		for (Serializable param : params)
			cmd.addParameter(param);
		return cmd;
	}

	/**
	 * Return the {@link GotoPageCommand} used to navigate to the page
	 * represented by the page token
	 * 
	 * @param pageToken
	 *            the page token
	 * @param session
	 *            a custom page request session
	 */
	public GotoPageCommand gotoPage(String pageToken, PageRequestSession session) {
		return new GotoPageCommand(pageToken, session, this);
	}

	/**
	 * Return the {@link GotoPageCommand} used to navigate to the page
	 * represented by the page token
	 * 
	 * @param pageToken
	 *            the page token
	 * @param session
	 *            a custom page request session
	 * @param convienance
	 *            method for using
	 *            {@link GotoPageCommand#addParameter(Serializable)}
	 */
	public GotoPageCommand gotoPage(String pageToken,
			PageRequestSession session, Serializable... params) {
		GotoPageCommand cmd = new GotoPageCommand(pageToken, session, this);
		for (Serializable param : params)
			cmd.addParameter(param);
		return cmd;
	}

	/**
	 * Use {@link GotoPageCommand#execute()}
	 */
	void gotoPage(GotoPageCommand command) {
		keepGoing = true;
		for (PageEventHandler handler : settings.getEventHandlers()) {
			handler.onPageRequest(command.getPageToken(), null,
					command.getSession());
		}
		String pageToken = command.getPageToken();
		LoadedPageContainer pageData = pageMap.get(pageToken);
		String historyToken = null;
		PageParameters parameters = null;
		if (null != command.getParameterList(false)) {
			Serializable[] params = command.getParameterList(false).toArray(
					new Serializable[command.getParameterList(false).size()]);
			historyToken = createHistoryToken(pageToken, params);
			parameters = new SimplePageParameters(pageToken, historyToken,
					params, null);
		} else if (null != command.getParameterMap(false)) {
			historyToken = createHistoryToken(pageToken,
					command.getParameterMap(false));
			parameters = new SimplePageParameters(pageToken, historyToken,
					null, command.getParameterMap(false));
		} else {
			historyToken = createHistoryToken(pageToken);
			parameters = new SimplePageParameters(pageToken, historyToken,
					null, null);
		}

		if (command.isAddHistoryToken())
			History.newItem(historyToken, false);

		if (null != pageData) {
			gotoPage(pageData, parameters, command);
		} else {
			settings.getPageLoader().getPage(pageToken,
					new _PageLoadCallback(parameters, command));
		}
	}

	private void onNewPage(String historyToken, boolean addHistoryToken) {
		keepGoing = true;
		for (PageEventHandler handler : settings.getEventHandlers()) {
			handler.onPageRequest(null, historyToken, null);
		}
		if (!keepGoing())
			return;
		String pageToken = null;
		if (null == historyToken
				|| historyToken.equals(PageLoader.PAGE_DEFAULT)) {
			pageToken = PageLoader.PAGE_DEFAULT;
		} else {
			Iterator<String> possibleTokens = settings.getTokenizer()
					.getPossiblePageTokens(historyToken);
			while (possibleTokens.hasNext()) {
				String token = possibleTokens.next();
				if (settings.getPageLoader().isValidPageToken(token)) {
					pageToken = token;
					break;
				}
			}
		}
		if (null == pageToken) {
			onPageNotFound(null, historyToken);
			return;
		}

		PageParameters parameters = settings.getTokenizer().getPageParameters(
				historyToken, pageToken);
		LoadedPageContainer pageLoadResult = pageMap.get(pageToken);
		if (null != pageLoadResult) {
			// the page was cached
			gotoPage(pageLoadResult, parameters, new GotoPageCommand(pageToken,
					this).addHistoryToken(addHistoryToken));
		} else {
			// the page couldn't be found
			settings.getPageLoader().getPage(
					pageToken,
					new _PageLoadCallback(parameters, new GotoPageCommand(
							pageToken, this).addHistoryToken(addHistoryToken)));
		}
	}

	/**
	 * Internal check - should we continue with the page request?
	 */
	private boolean keepGoing() {
		if (keepGoing)
			return true;
		else
			return false;
	}

	/** Async page loading callbacks **/
	private void onPageFound(LoadedPageContainer pageLoadResult,
			PageParameters pageParameters, GotoPageCommand command) {
		for (PageEventHandler handler : settings.getEventHandlers()) {
			handler.onPageLoaded(pageLoadResult);
		}
		if (!keepGoing())
			return;
		pageMap.put(pageLoadResult.getPageToken(), pageLoadResult);
		gotoPage(pageLoadResult, pageParameters, command);
	}

	private void onPageNotFound(String pageToken, String historyToken) {
		for (PageEventHandler handler : settings.getEventHandlers()) {
			handler.onPageNotFound(historyToken);
		}
	}

	private void onPageLoadFailure(String pageToken, String historyToken,
			Throwable cause) {
		for (PageEventHandler handler : settings.getEventHandlers()) {
			handler.onPageLoadFailure(historyToken, cause);
		}
	}

	private void gotoPage(LoadedPageContainer page, PageParameters parameters,
			GotoPageCommand command) {
		command.getSession().setPageAttributes(page.getAttributes());
		page = page.copy();
		lastCommand = command;
		for (PageEventHandler handler : settings.getEventHandlers()) {
			handler.onBeforePageEnter(page, parameters, command);
		}
		if (!keepGoing())
			return;
		LoadedPageContainer previousPage = null;
		try {
			page.getPage().onEnter(
					parameters,
					command.getSession(),
					new _AsyncPageCallback(page, previousPage, parameters,
							command));
			for (PageEventHandler handler : settings.getEventHandlers()) {
				handler.onAfterPageEnter(page, parameters, command);
			}
		} catch (Throwable t) {
			for (PageEventHandler handler : settings.getEventHandlers()) {
				handler.onPageEnterFailure(page, parameters, command);
			}
		}
	}

	/**
	 * History token change listener
	 */
	public void onValueChange(ValueChangeEvent<String> event) {
		onNewPage(event.getValue(), true);
	}

	/**
	 * Create and return the history token represented by the page token and
	 * additional parameters
	 * 
	 * @param pageToken
	 *            the page token
	 * @param parameters
	 *            input parameters
	 */
	public String createHistoryToken(String pageToken,
			Serializable... parameters) {
		return settings.getTokenizer()
				.createHistoryToken(pageToken, parameters);
	}

	/**
	 * Create and return the history token represented by the page token and
	 * additional parameters
	 * 
	 * @param pageToken
	 *            the page token
	 * @param parameters
	 *            input parameters
	 */
	public String createHistoryToken(String pageToken,
			Map<String, Serializable> parameters) {
		return settings.getTokenizer()
				.createHistoryToken(pageToken, parameters);
	}

	/**
	 * Stop the current request processing
	 */
	public void stopRequest() {
		keepGoing = false;
	}

	public GotoPageCommand getLastCommand() {
		return lastCommand;
	}

	/**
	 * Add page lifecycle event handler
	 * 
	 * @param pageEventHandlers
	 *            the handler(s)
	 * @return the Settings
	 */
	public Pages add(PageEventHandler... pageEventHandlers) {
		for (PageEventHandler pageEventHandler : pageEventHandlers) {
			settings.getEventHandlers().add(pageEventHandler);
		}
		return this;
	}

	/**
	 * Set the {@link Tokenizer}. If not set, {@link SimpleTokenizer} will be
	 * used.
	 * 
	 * @param tokenizer
	 *            the page tokenizer
	 * @return the Settings
	 */
	public Pages setPageTokenizer(Tokenizer tokenizer) {
		settings.tokenizer = tokenizer;
		tokenizer.init(this);
		return this;
	}

	/**
	 * Add default event handlers for handling page request messages as well as
	 * notifying the user when pages are being loaded. The handlers that are
	 * added are
	 * <ul>
	 * <ol>
	 * <li>{@link DefaultPageLoadingHandler}</li>
	 * <li>{@link DefaultPageMessageHandler}</li>
	 * </ul>
	 */
	public Pages addDefaultEventHandlers() {
		add(new DefaultPageLoadingHandler(), new DefaultPageMessageHandler());
		return this;
	}

	/**
	 * Return the settings for this pages instance
	 */
	public Settings getSettings() {
		return settings;
	}

	/**
	 * Return the common event bus
	 */
	public HandlerManager getEventBus() {
		return eventBus;
	}

	/** Async callback classes */
	private class _AsyncPageCallback implements AsyncPageCallback {
		private LoadedPageContainer previousPage;
		private LoadedPageContainer page;
		private PageParameters pageParameters;
		private GotoPageCommand command;

		private _AsyncPageCallback(LoadedPageContainer page,
				LoadedPageContainer previousPage,
				PageParameters pageParameters, GotoPageCommand command) {
			this.page = page;
			this.previousPage = previousPage;
			this.pageParameters = pageParameters;
			this.command = command;
		}

		public void onSuccess() {
			settings.getApplicationPresenter().showPage(page, pageParameters,
					command.getSession());
			for (PageEventHandler handler : settings.getEventHandlers()) {
				handler.onPageEnterSuccess(page, pageParameters, command);
			}
			getEventBus().fireEvent(new PageShownEvent(page, previousPage));
		}

		public void waitForAsync() {
			for (PageEventHandler handler : settings.getEventHandlers()) {
				handler.onPageWaitForAsync(page, pageParameters, command);
			}
		}

		public void onFailure(Throwable cause) {
			for (PageEventHandler handler : settings.getEventHandlers()) {
				handler.onPageEnterFailure(page, pageParameters, command);
			}
		}
	}

	private class _PageLoadCallback implements PageLoadCallback {
		private PageParameters parameters;
		private GotoPageCommand command;

		public _PageLoadCallback(PageParameters parameters,
				GotoPageCommand command) {
			this.parameters = parameters;
			this.command = command;
		}

		public void onPageFound(LoadedPageContainer pageLoadResult) {
			Pages.this.onPageFound(pageLoadResult, parameters, command);
		}

		public void onPageNotFound(String pageToken) {
			Pages.this.onPageNotFound(pageToken, parameters.getHistoryToken());
		}

		public void onPageLoadFailure(String pageToken, Throwable cause) {
			Pages.this.onPageLoadFailure(pageToken,
					parameters.getHistoryToken(), cause);
		}
	}

	public class Settings {

		protected ApplicationPresenter pagePresenter;
		protected Tokenizer tokenizer;
		protected PageLoader pageLoader;
		protected ArrayList<PageEventHandler> pageEventHandlers = new ArrayList<PageEventHandler>();

		/**
		 * Return the {@link Tokenizer}. If not set, {@link SimpleTokenizer}
		 * will be used.
		 */
		public Tokenizer getTokenizer() {
			if (null == tokenizer) {
				tokenizer = new SimpleTokenizer();
				tokenizer.init(Pages.this);
			}
			return tokenizer;
		}

		/**
		 * Return a list of all defined page lifecycle event handlers (or empty
		 * list if none defined)
		 */
		public ArrayList<PageEventHandler> getEventHandlers() {
			return pageEventHandlers;
		}

		/**
		 * Return the {@link PageLoader}.
		 */
		public PageLoader getPageLoader() {
			return pageLoader;
		}

		/**
		 * Return the {@link ApplicationPresenter}
		 */
		public ApplicationPresenter getApplicationPresenter() {
			return pagePresenter;
		}
	}
}